var printf = require('printf'),
    json = require('json');

var oil = require('oil'),
    log = require('oil/log'),
    path = require('oil/path');

// The abstractions PackageSet and MaybePackage try to perform the minimal
// number of system calls in order to successfully load modules.
// 
// Basically to load modules, we just need some opendir()/readdir()/listdir()
// calls, and then try to open() package.json, and then try to open() the
// module.  Shouldn't need any stats.
//
// NOTE: It doesn't seem to be worth reducing readdir() calls, because those are
// just libc calls that translate to a single getdents() sys call (?)


var ModuleNotFound = exports.ModuleNotFound = function(name, filename) {
  this.name = name;
  this.filename = filename;
}

ModuleNotFound.prototype = {
  toString: function() {
    return printf.sprintf(
        "FATAL: Can't resolve module '%s' from %s", this.name, this.filename);
  }
};

// Encapsulates a directory that might be a package.  This makes sure we read
// package.json 0 or 1 times (lazily, memoized)
//
// Args:
//   fs: FileSystem instance at the potential package root.  It's OK if the root
//   isn't even a directory, much less a package.
var MaybePackage = exports.MaybePackage = function(fs) {
  this.fs = fs;
  this._metadata = undefined;
}

MaybePackage.prototype = {

  // Try to find the given module in this package.
  //
  // We do this *without regard* to whether its a package yet!  In the common
  // case, a module won't resolve in a MaybePackage, so we don't have to bother
  // testing if it's package or not.
  findModule: function(moduleName, engine) {
    // If we already KNOW we're not a package due to a metadata() call, don't
    // even bother trying to open the file.
    if (this._metadata === null) {
      return [];
    }

    // TODO: either memoize the open() call, or do it at a high level
    //
    // TODO: Look in engines/default/lib for json?
    // That is part of a package.  Also might need some of 'json'

    var templates = [
        ['lib', '?.js'],
        ['engines', 'default', 'lib', '?.js']
        ];

    if (engine) {
      templates.push(['engines', engine, '?.js']);
    }

    var results = [];
    for (var i=0; i<templates.length; i++) {
      var t = path.normalize.apply(null, templates[i]);
      var result = this.getModule(moduleName, t);
      if (result) {
        results.push(result);
      }
    }
    return results;
  },

  // Retrieves a module
  // Returns:
  //   Either null, or an open file handle and a full path
  getModule: function(moduleName, template) {
    var relPath = template.replace(/\?/, moduleName),
        fullPath = this.fs.path(relPath);
    try {
      log.debug('Trying %s', fullPath);
      var handle = this.fs.open(relPath);
      log.info('Found module at %s',  fullPath);
      return {handle: handle, fullPath: fullPath};
    } catch (e) {
      return null;
    }
  },

  // Read a package.json and return it, or null.  Memoized so that we only try
  // reading package.json once per MaybePackage instance.
  metadata: function() {
    if (this._metadata === undefined) {
      var contents = this.fs.contentsOf('package.json', null);
      if (contents) {
        try {
          this._metadata = json.parse(contents);
        } catch (e) {
          log.error("Can't parse metadata in %s: %s",
              this.fs.path('package.json'), e);
          this._metadata = null;
        }
      } else {
        this._metadata = null;
      }
    }
    return this._metadata;
  }

};

// You can search for a module in a set of packages.
//
// Args:
//   maybes: An array of MaybePackage instances
var PackageSet = exports.PackageSet = function(maybes) {
  this.maybes = maybes;
}

PackageSet.prototype = {

  // Args:
  //   moduleName: e.g. 'util/log', from require('util/log')
  //   engine: e.g. 'node'
  //   filename: Only for error reporting
  // Returns:
  //   An array of objects with handle/fullPath attributes, or null if the
  //   module can't be found
  findModule: function(moduleName, engine, filename) {
    // This part will just test lib/foo.js, engines/default/foo.js, without
    // regard to whether its a package or not
    var candidates = [];
    for (var i=0; i<this.maybes.length; i++) {
      var m = this.maybes[i],
          files = m.findModule(moduleName, engine),
          candidate = {};

      if (files.length !== 0) {
        // A candidate has a fullPath, file handle, and a MaybePackage instance
        candidate.maybePackage = m;
        candidate.files = files;
        candidates.push(candidate);
      }
    }

    log.info('%d candidates', candidates.length);

    // Now we have a bunch of candidates.  Pick the first candidate that
    // actually *is* a package.
    var first = null; 
    for (var i=0; i<candidates.length; i++) { 
      var c = candidates[i],
          metadata = c.maybePackage.metadata();
      if (metadata) {
        first = c;
        break;
      }
    }

    // Close all open file handles except the one we're returning
    for (var i=0; i<candidates.length; i++) {
      var c = candidates[i];
      if (c === first) {
        continue;
      }
      for (var j=0; j<c.files.length; j++) {
        c.files[j].handle.close();
      }
    }
    
    if (first) {
      return first.files;
    } else {
      throw new ModuleNotFound(moduleName, filename);
    }
  }

};


exports.makePackages = makePackages = function(packagesList) {
  var maybes = [];

  // Now add all the individal paths
  for (var i=0; i<packagesList.length; i++) {
    var fs = new oil.FileSystem(packagesList[i]);
    maybes.push(new MaybePackage(fs));
  }

  return new PackageSet(maybes);
};
